package org.osite.xml;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.osite.util.Nullable;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class XmlUtil {

    private static DocumentBuilder documentBuilder = null;

    private XmlUtil() {
    }

    public synchronized static Document newDocument() {
        try {
            if (documentBuilder == null) {
                documentBuilder = DocumentBuilderFactory.newInstance()
                        .newDocumentBuilder();
            }
            return documentBuilder.newDocument();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }
    }

    static public String getOptionalAttribute(Element element, String name,
            String defaultValue) {
        return element.hasAttribute(name) ? element.getAttribute(name)
                : defaultValue;
    }

    public static Iterable<Node> listChildNodes(Node node) {
        return listChildNodes(node, null);
    }

    public static <T extends Node> Iterable<T> listChildNodes(final Node node,
            final NodeFilter filter) {
        return new Iterable<T>() {
            @Override
            public Iterator<T> iterator() {
                return new ChildNodesIterator<T>(node, filter);
            }
        };
    }

    public static Iterable<Element> listChildElements(Element element) {
        return listChildNodes(element, new NodeFilter() {
            public boolean accept(Node node) {
                return (node.getNodeType() == Node.ELEMENT_NODE);
            }
        });
    }

    private static interface NodeFilter {
        public boolean accept(Node node);
    }

    private static class ChildNodesIterator<T> implements Iterator<T> {
        private final NodeFilter filter;
        private Node next;

        public ChildNodesIterator(Node parent, @Nullable NodeFilter filter) {
            this.filter = filter;
            next = parent.getFirstChild();
            findNext();
        }

        private void findNext() {
            while ((next != null) && (filter != null) && !filter.accept(next)) {
                next = next.getNextSibling();
            }
        }

        @Override
        public boolean hasNext() {
            return (next != null);
        }

        @Override
        @SuppressWarnings("unchecked")
        public T next() {
            Node result = next;
            next = next.getNextSibling();
            findNext();
            return (T) result;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    public static Element[] getChildElements(Element parent) {
        ArrayList<Element> result = new ArrayList<Element>();

        NodeList list = parent.getChildNodes();
        for (int i = 0, n = list.getLength(); i < n; ++i) {
            if (list.item(i) instanceof Element) {
                result.add((Element) list.item(i));
            }
        }
        return result.toArray(new Element[result.size()]);
    }

    public static Element getFirstElement(Element element) {
        NodeList list = element.getChildNodes();
        for (int i = 0, n = list.getLength(); i < n; ++i) {
            if (list.item(i) instanceof Element) {
                return (Element) list.item(i);
            }
        }
        return null;
    }

    public static Document parseDocument(InputStream is) throws IOException {
        try {
            DOMResult result = new DOMResult();
            TransformerFactory.newInstance().newTransformer().transform(
                    new StreamSource(is), result);
            return (Document) result.getNode();
        } catch (TransformerException tfe) {
            throw new IOException(tfe);
        }
    }

    public static Document parseDocument(StringReader reader)
            throws IOException {
        try {
            DOMResult result = new DOMResult();
            TransformerFactory.newInstance().newTransformer().transform(
                    new StreamSource(reader), result);
            return (Document) result.getNode();
        } catch (TransformerException tfe) {
            throw new IOException(tfe);
        }
    }

    private static String getNodeIdentity(Node node) {
        Node parent = node.getParentNode();
        if (parent == null) {
            return node.getNodeName();
        }
        NodeList list = parent.getChildNodes();
        int index = 0;
        for (int i = 0, n = list.getLength(); i < n; ++i) {
            if (node == list.item(i)) {
                return node.getNodeName() + "[" + index + "]";
            } else if (list.item(i).getNodeName().equals(node.getNodeName())) {
                ++index;
            }
        }
        throw new IllegalArgumentException();
    }

    public static void printNode(Node node) {
        try {
            printNode(node, System.out);
        } catch (TransformerException e) {
            throw new RuntimeException(e);
        }
    }

    public static void printNode(Node node, OutputStream os)
            throws TransformerException {
        try {
            Transformer transformer = TransformerFactory.newInstance()
                    .newTransformer();
            transformer.setOutputProperty("omit-xml-declaration", "yes");
            transformer.setOutputProperty("indent", "yes");
            transformer.transform(new DOMSource(node), new StreamResult(os));
        } catch (TransformerConfigurationException e) {
            throw new RuntimeException(e);
        } catch (TransformerFactoryConfigurationError e) {
            throw new RuntimeException(e);
        }
    }

    public static String getNodePath(Node node) {
        StringBuilder builder = new StringBuilder();
        while (node != null && (node.getNodeType() != Node.DOCUMENT_NODE)) {
            builder.insert(0, getNodeIdentity(node));
            switch (node.getNodeType()) {
            case Node.ATTRIBUTE_NODE:
                builder.insert(0, '#');
                break;
            default:
                builder.insert(0, '.');
                break;
            }
            node = ((node.getNodeType() == Node.ATTRIBUTE_NODE) ?
                    ((Attr) node).getOwnerElement() : node.getParentNode());
        }
        return builder.toString();
    }

    public static void removeChildren(Node parent) {
        NodeList list = parent.getChildNodes();
        for (int i = 0, n = list.getLength(); i < n; ++i) {
            parent.removeChild(list.item(i));
        }
    }

}
